DynamoDB でスロットリングエラーを発生させてみた

DynamoDB でスロットリングエラーを発生させてみた

DynamoDB のスロットリングを意図的に発生させ、挙動を確認してみました。
Clock Icon2024.08.18

コーヒーが好きな emi です。

DynamoDB の性能がなかなか思うように上がらず、パフォーマンス向上のため環境調査をしていたところ、スロットリングが発生していることに気が付きました。

スロットリング (throttling) とは、システムの過負荷や特定のアクセスによるリソース独占を回避するため、一定の制限値を超えた場合に意図的に性能を低下させたり、要求を一時的に拒否したりする制御のことです。

DynamoDB のスロットリングの仕組みについて理解するために、意図的にスロットリングを発生させてみました。

構成図

今回は Lambda から DynamoDB に大きなサイズのアイテム書き込み(PutItem)を行い、意図的にスロットリングエラーを発生させます。

emiki_simulating-dynamodb-throttling-errors_1

スロットリングさせる際の考慮事項

詳細は検証手順の中で再度案内しますが、特に以下 3 点考慮します。

  • 1. 意図的にスロットリングさせるため、DynamoDB テーブルのスペックは固定(プロビジョンドモード)で低く設定する
    • 十分なキャパシティユニットが確保されているとスロットリングが発生しません。オンデマンドモードや AutoScalling が有効になっているとキャパシティユニットが確保されてしまうためスロットリングが発生しません。
  • 2. DynamoDB にはバーストキャパシティがあるため、スロットリングさせる場合は書き込むアイテムのサイズを十分に大きくする
    • DynamoDB には バーストキャパシティ という仕組みがあり、利用可能なスループットを使い切っていない場合、キャパシティの未使用分を最大 5 分 (300 秒) 蓄えておき、後のスループットのバースト(スパイク)時に消費することで対応します。
    • つまり、低く設定した WCU や RCU を少し上回るくらいでは貯蓄された余剰キャパシティを消費することによりスロットリングが発生しません。
    • 書き込むアイテムのサイズは十分に大きくし、できれば繰り返し書き込むことでスロットリング発生確率を高くします。
  • 3. Boto3 の再実行ロジックを明示的に無効にする(リトライを 0 回に指定する)
    • AWS SDK には実行時のエラーを受け取った際の再試行ロジックが最初から自動で実装されており、ユーザーが明示的にリトライロジックを組まなくても各リソースのデフォルトで設定された回数分はエクスポネンシャルバックオフによるリトライ処理が実施されます。
    • よって、今回はスロットリングを発生させるためデフォルトのリトライ回数を 0 回に変更します。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/burst-adaptive-capacity.html

https://docs.aws.amazon.com/ja_jp/sdkref/latest/guide/feature-retry-behavior.html

DynamoDB テーブルの作成

DynamoDB テーブルを作成します。
テーブル名は「throttled-test」、パーティションキーは throttled-test-key としました。

1. 意図的にスロットリングさせるため、DynamoDB テーブルのスペックは固定(プロビジョンドモード)で低く設定する

意図的にスロットリングさせるため、DynamoDB テーブルのスペックは固定で低く設定します。
キャパシティモードはプロビジョンドモードにし、Auto Scaling はオフにします。

  • プロビジョンされた読み込みキャパシティーユニット(RCU):1
  • プロビジョンされた書き込みキャパシティーユニット(WCU):1

ちなみに、オンデマンドモードで作成した DynamoDB テーブルをプロビジョンドモードに変更すると、21~22 時間程度オンデマンドモードに戻せなくなります。
emiki_simulating-dynamodb-throttling-errors_2

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-switching-capacity-modes.html

Lambda 関数の作成

DynamoDB にレコードを書き込む Lambda 関数を作成します。
関数名は「dynamodb_data_put」とし、ランタイムは Python 3.12 にしました。アーキテクチャはコストを抑えるため arm64 にしてみました。
デフォルトの実行ロールは「基本的な Lambda アクセス権限で新しいロールを作成」で進めます。後ほど DynamoDB への書き込み権限を追加します。
他はデフォルト設定のまま、関数を作成します。

emiki_simulating-dynamodb-throttling-errors_3

Lambda 関数に DynamoDB への書き込み権限を付与

DynamoDB にアイテムを書き込むための PutItem アクション許可を追加します。

IAM コンソールで以下のような新規カスタム管理ポリシーを作成してください。今回ポリシー名は「dynamodb_data_put-policy」としました。
arn:aws:dynamodb:ap-northeast-1:123456789012:table/throttled-test には自身が作成した DynamoDB テーブルの ARN を入れます。

dynamodb_data_put-policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "dynamodb:PutItem",
            "Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/throttled-test"
        }
    ]
}

Lambda コンソールに戻り、作成した Lambda 関数「dynamodb_data_put」をクリックし、[設定]タブ - [アクセス権限] からロール名のリンクをクリックして IAM ロールの画面に遷移します。

emiki_simulating-dynamodb-throttling-errors_4

あらかじめ作成しておいたカスタム管理ポリシー「dynamodb_data_put-policy」を、Lambda の実行ロールにアタッチします。

emiki_simulating-dynamodb-throttling-errors_5

これで、Lambda 関数から DynamoDB にアイテムを書き込むことができるようになりました。

DynamoDB に PutItem する Python コードの作成

今回は Python 3.12 を使用します。Python で DynamoDB を使用する方法は以下のドキュメントが参考になります。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/programming-with-python.html

以下のコードを「lambda_function.py」に貼り付けます。

lambda_function.py
import json
import random
import string
from botocore.client import Config
import boto3

# リトライ回数を 0 に設定
config = Config(
    retries={
        'max_attempts': 0 # リトライ回数を 0 に設定
    }
)

# boto3 を使用して DynamoDB クライアントを作成(初期化)
dynamodb_client = boto3.client(
    'dynamodb',
    config=config,
    region_name='ap-northeast-1'
    )

# 大きなサイズのデータを生成する関数を定義
def generate_large_data(size_in_kb):
    # ASCII 文字と数字を組み合わせて指定サイズのデータを生成
    # size_in_kb は生成するデータのサイズを KB 単位で指定
    return ''.join(random.choices(string.ascii_letters + string.digits, k=size_in_kb * 1024))

# データを生成
data = generate_large_data(256) # 256KB のデータを生成

i = 0 # カウンタを初期化

# スロットリングを発生させるために、同じテーブルに複数のアイテムを書き込む
while i < 10:
    item = {
        'throttled-test-key': {'S': f'aaa-{i}'}, # S で String 型を指定、各アイテムにユニークなパーティションキーを設定。パーティションキーは throttled-test-key (String) でテーブルに作成済み
        'foo': {'S': data} # 生成した 256KB のデータを設定
    }
    # DynamoDB に書き込みを実行
    dynamodb_client.put_item(
        TableName='throttled-test',
        Item=item
    )
    print(f'Item{i}written')
    i += 1

コードの解説

DynamoDB に対するアクセスには boto3 の client を使用してみました。

# boto3 を使用して DynamoDB クライアントを作成(初期化)
dynamodb_client = boto3.client(
    'dynamodb',
    config=config,
    region_name='ap-northeast-1'
    )

https://dev.classmethod.jp/articles/boto3-client-dynamodb-sample/

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/client/put_item.html

2. DynamoDB にはバーストキャパシティがあるため、スロットリングさせる場合は書き込むアイテムのサイズを十分に大きくする

DynamoDB には バーストキャパシティ という仕組みがあり、利用可能なスループットを使い切っていない場合、キャパシティの未使用分を最大 5 分 (300 秒) 蓄えておき、後のスループットのバースト(スパイク)時に消費することで対応します。低く設定した WCU や RCU を少し上回るくらいでは貯蓄された余剰キャパシティを消費することによりスロットリングが発生しないため、書き込むアイテムのサイズは十分に大きくし、繰り返し書き込むようにします。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/burst-adaptive-capacity.html

データ生成のために Python 標準ライブラリの randomstring を使います。

# 大きなサイズのデータを生成する関数を定義
def generate_large_data(size_in_kb):
    # ASCII 文字と数字を組み合わせて指定サイズのデータを生成
    # size_in_kb は生成するデータのサイズを KB 単位で指定
    return ''.join(random.choices(string.ascii_letters + string.digits, k=size_in_kb * 1024))

# データを生成
data = generate_large_data(256) # 256KB のデータを生成

生成した 256KB の文字列を、アイテムとしてテーブルに 10 回書き込んでいきます。パーティションキー throttled-test-key に入る文字列は各アイテムで被らないように i += 1 しながら繰り返します。

i = 0 # カウンタを初期化

# スロットリングを発生させるために、同じテーブルに複数のアイテムを書き込む
while i < 10:
    item = {
        'throttled-test-key': {'S': f'aaa-{i}'}, # S で String 型を指定、各アイテムにユニークなパーティションキーを設定。パーティションキーは throttled-test-key (String) でテーブルに作成済み
        'foo': {'S': data} # 生成した 256KB のデータを設定
    }
    # DynamoDB に書き込みを実行
    dynamodb_client.put_item(
        TableName='throttled-test',
        Item=item
    )
    print(f'Item{i}written')
    i += 1

今回 DynamoDB テーブル作成の際、プロビジョンドキャパシティを WCU = 1 としました。最大サイズが 1 KB の項目について 1 秒あたり 1 回の書き込みで 1WCU 消費しますから、256KB を 1 アイテムに突っ込もうとすればそれだけで 256WCU 消費します。バーストキャパシティも尽きるはずです。
さらにそれを 10 回繰り返そうものなら、きっとひとたまりもなくスロットリングしてくれることでしょう。

ちなみに DynamoDB には以下のクォーターがあるので、指定する文字列のサイズは大きすぎないようにします。

  • アイテムの最大サイズは 400 KB
  • クエリやスキャンの response サイズは 1MB

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/ServiceQuotas.html

3. Boto3 の再実行ロジックを明示的に無効にする(リトライを 0 回に指定する)

AWS SDK には実行時のエラーを受け取った際の再試行ロジックが最初から自動で実装されており、ユーザーが明示的にリトライロジックを組まなくても各リソースのデフォルトで設定された回数分はエクスポネンシャルバックオフによるリトライ処理が実施されます。

https://docs.aws.amazon.com/ja_jp/sdkref/latest/guide/feature-retry-behavior.html

すごい、便利ですね!

botocore のソースコードを見ると "max_attempts": 5, となっており、既定のリトライ回数は 5 回です。

https://github.com/boto/botocore/blob/develop/botocore/data/_retry.json#L93

とっても便利な仕組みなのですが、今回はスロットリングを意図的に発生させるために、このリトライ回数を 0 回に変更しておきます。

# リトライ回数を 0 に設定
config = Config(
    retries={
        'max_attempts': 0 # リトライ回数を 0 に設定
    }
)

リトライ回数の設定方法は下記記事を参照ください。

https://dev.classmethod.jp/articles/how-to-change-error-number-retry-boto3-ja/

スロットリングを発生させる

準備ができたので Lambda 関数を実行し、DynamoDB テーブルに書き込みを行います。
Lambda コンソールで「テスト」タブを開き、「テスト」をクリックします。

emiki_simulating-dynamodb-throttling-errors_6

エラーになりました。
赤枠の一番上に表示される実行ログの最後の 4KB の部分を見ると、3 秒でタイムアウトした旨が確認できます。

下部の「ログ出力」部分を見ると、[ERROR] ProvisionedThroughputExceededException: と表示されている行があります。
良いですね。これは想定通りスロットリングが起こったログです。

ログ出力
Item0written
Item1written
[ERROR] ProvisionedThroughputExceededException: An error occurred (ProvisionedThroughputExceededException) when calling the PutItem operation (reached max retries: 0): The level of configured provisioned throughput for the table was exceeded. Consider increasing your provisioning level with the UpdateTable API.
Traceback (most recent call last):
  File "/var/lang/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 995, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/var/task/lambda_function.py", line 39, in <module>
    dynamodb_client.put_item(
  File "/var/lang/lib/python3.12/site-packages/botocore/client.py", line 565, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/var/lang/lib/python3.12/site-packages/botocore/client.py", line 1021, in _make_api_call
    raise error_class(parsed_response, operation_name)INIT_REPORT Init Duration: 809.64 ms	Phase: init	Status: error	Error Type: Runtime.Unknown
INIT_REPORT Init Duration: 3026.47 ms	Phase: invoke	Status: timeout
START RequestId: c9ffa6e4-8b14-4a5a-8805-98ceed4dac7b Version: $LATEST
END RequestId: c9ffa6e4-8b14-4a5a-8805-98ceed4dac7b
REPORT RequestId: c9ffa6e4-8b14-4a5a-8805-98ceed4dac7b	Duration: 3000.00 ms	Billed Duration: 3000 ms	Memory Size: 128 MB	Max Memory Used: 74 MB	Status: timeout

Lambda のログが保存されている CloudWatch Log グループ /aws/lambda/dynamodb_data_put で詳細な情報も確認できます。
一番上の最新のログストリームを見てみましょう。

emiki_simulating-dynamodb-throttling-errors_7

以下のことが読み取れます。

  • Item0written と Item1written で 2 アイテムが正常に書き込まれている
  • [ERROR] ProvisionedThroughputExceededException で、PutItem 操作の際プロビジョニングされたスループットを超え、書き込みが失敗している
  • REPORT ~ Duration: 3000.00 ms Billed Duration: 3000 ms ~ Status: timeout で Lambda 関数がタイムアウト(3000ms)に達し途中で処理が終了

emiki_simulating-dynamodb-throttling-errors_8

2 アイテム書き込みを行ったところでスロットリングが発生し、そのまま Lambda 関数のデフォルトタイムアウト時間 3 秒(3000ms)が経過して実行が終わったということですね。

プロビジョンドキャパシティで WCU = 1 としているテーブルで 256KB の文字列を 2 回も書き込めているので、計算上 512 WCU は消費したはずです。バーストキャパシティは侮れませんね。

DynamoDB テーブル「throttled-test」の概要タブ下部で、キャパシティメトリクスを見てみましょう。

書き込み使用量 (平均単位/秒)(Write usage (average units/second))グラフで、プロビジョンドされている 1WCU の赤いラインを、実際に消費した WCU の青いラインが大幅に超えている瞬間があるのが分かります。二回超えていますが、これは私が二回 Lambda 関数をテスト実行したからです。

書き込み調整されたリクエスト (数)(Write throttled requests (count))グラフが、スロットリングしたリクエストの回数です。PutItem を示す青いラインが表示され、スロットリングが発生しているのが分かります。

emiki_simulating-dynamodb-throttling-errors_9

「調整された」という日本語が分かりにくいのですが、スロットリングのことです。英語にした方が分かりやすいかもしれませんね。

emiki_simulating-dynamodb-throttling-errors_10

最後に、DynamoDB のテーブルに書き込んだアイテムの様子を見てみましょう。DynamoDB コンソールで対象のテーブルを開き、右上の「テーブルアイテムの探索」というオレンジのボタンをクリックすると、格納されたアイテムが表示できます。

emiki_simulating-dynamodb-throttling-errors_11

2 アイテム書き込まれていますね。foo には長いランダム文字列が二つ、同じものが格納されているのが分かります。

おわりに

DynamoDB のスロットリングの挙動のイメージがつかめたと思います。ついでに Lambda の仕様や、AWS SDK で DynamoDB にアクセスする方法も勉強できました。どなたかのお役に立てば幸いです。

余談

余談なのですが、Boto はアマゾン川に生息する淡水イルカの名前から取られているそうです。なんだか可愛く感じられてきましたね!ドキュメントにこういうことが書かれているとちょっと楽しいです。

Boto (ボトと発音) という名前は、アマゾン川に自生する淡水イルカに由来しています。
Python と Boto3 による Amazon DynamoDB のプログラミング - Amazon DynamoDB

日本語ではアマゾンカワイルカと呼ぶそうで、口が長くピンク色をしている珍しいイルカだそうです。

参考

https://dev.classmethod.jp/articles/raise-dynamodb-throughput-error-manually/

https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Operations.html

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.